/***************************************************************
*  Copyright (c) 2008 by GroupMe! Team (www.groupme.net)
*  All rights reserved.
*
*  This file is part of the GroupMe! Project. Source code of 
*  this project is closed and redistribution of this code is
*  prohibited. 
*  
*  Contact: http://www.groupme.net
*
*  This copyright notice MUST APPEAR in all copies of the file!
***************************************************************/
package net.groupme.search.strategy.social;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import net.groupme.model.Identifiable;
import net.groupme.model.ObjectFactory;
import net.groupme.search.strategy.model.WeightedIdentifiableComparatorByWeight;
import net.groupme.search.strategy.model.WeightedResource;
import net.groupme.utils.sql.SelectQueryUtility;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import cern.colt.matrix.DoubleMatrix1D;
import cern.colt.matrix.DoubleMatrix2D;
import cern.colt.matrix.impl.DenseDoubleMatrix1D;
import cern.colt.matrix.impl.SparseDoubleMatrix2D;
import cern.colt.matrix.linalg.Algebra;

/**
 * created on May 27, 2008
 * 
 * This class defines static methods to compute the SocialPageRank for resources
 * within the GroupMe! data set.
 * 
 * @author Fabian Abel, <a href="mailto:abel@l3s.de">abel@l3s.de</a>
 * @author last edited by: $Author: fabian $
 * 
 * @version $Revision: 1.4 $ $Date: 2008-05-30 01:19:37 $
 */
public class SocialPageRank {
	
	/** Logger for this class */
	protected static final Log logger = LogFactory.getLog(SocialPageRank.class);

	/** the <code>M_PU</code> Matrix. M_PU(r_i, u_j) = how many annotations user u_j has assigned to resource r_i */
	private static DoubleMatrix2D resourceUserMatirx = null;
	
	/** the <code>M_AP</code> Matrix. M_AP(t_i, r_j) = how many users have assigned t_i to resource r_j */
	private static DoubleMatrix2D tagResourceMatirx = null;
	
	/** the <code>M_UA</code> Matrix. M_UA(u_i, t_j) = how many resources has user u_i annotated with t_j */
	private static DoubleMatrix2D userTagMatirx = null;
	
	/** convergence criteria for the SPR vector computation 
	 * (if average of abs(spr_k[i] - spr_k+1[i]) < convergenceCriteria 
	 *  then we will stop with the computation of the SPR vector  */
	public static double convergenceCriteria = 1E-20;
	/** Second halt criteria is the max number of iterations */
	public static int maxIterations = 100;
	
	/** the Factor for putting emphasis in a tag of interest */ 
	public static Double INFLUENCE_OF_PREFERENCE = 20.0;
	
	/** the SocialPageRankVector */
	private static List<WeightedResource> socialPageRank = null;
	
	public static final int NO_TAG_OF_INTEREST = -1;
	
	public static List<WeightedResource> getSocialPageRank(SPRPreference prefs, int topk, int indexOfTagOfInterest){
		if(socialPageRank == null || indexOfTagOfInterest != NO_TAG_OF_INTEREST){
			DoubleMatrix1D spr = computeSocialPageRank(prefs, indexOfTagOfInterest);
			
			socialPageRank = new ArrayList<WeightedResource>();
			ResultSet resourceSet = SelectQueryUtility.executeQuery("SELECT id, name, description, url, type FROM Resource");
			try {
				while(resourceSet.next()){
					socialPageRank.add(new WeightedResource(ObjectFactory.getInMemoryResource(resourceSet.getString(1), resourceSet.getString(2), resourceSet.getString(3), resourceSet.getURL(4), resourceSet.getString(5)),
							spr.get(resourceSet.getInt(1))));
				}
			} catch (SQLException e) {
				e.printStackTrace();
			}
			Collections.sort(socialPageRank, new WeightedIdentifiableComparatorByWeight());
		}
		
		if(socialPageRank.size() < topk){
			return socialPageRank;
		}
		return socialPageRank.subList(0, topk);
	}
	
	
	/**
	 * Computes the SocialPageRank for all resources of the data set.
	 * @param prefs tell us how to construct the matrices (which data to use)
	 * @return SocialPageRank vector for all resources of the data set
	 */
	public static DoubleMatrix1D computeSocialPageRank(SPRPreference prefs, int indexOfTagOfInterest){
		logger.info("Starting to compute SocialPageRank");
		if(resourceUserMatirx == null || indexOfTagOfInterest != NO_TAG_OF_INTEREST){
			initMatrices(prefs, indexOfTagOfInterest);
		}
		DoubleMatrix1D spr = new DenseDoubleMatrix1D(resourceUserMatirx.rows());
		Algebra alg = new Algebra();
		double residual = spr.size() * (convergenceCriteria + 1.0);
		double maxAllowedResidual = spr.size() * convergenceCriteria;
		double  actualSum = 0.0;
		int iterations = 0; double value = 0.0, previousValue = 0.0;
		
		//initialize all values of the SPR vector with the different random values: 
		for(int i=0; i < spr.size(); i++){
			spr.set(i, Math.random());	
		}
		actualSum = alg.norm1(spr);
		for(int i=0; i < spr.size(); i++){
			value = spr.get(i) / actualSum;
			spr.set(i, (Double.isNaN(value) ? 0.0 : value));
		}
		
		DoubleMatrix1D previousSPR = new DenseDoubleMatrix1D(resourceUserMatirx.rows());
		DoubleMatrix2D resourceUserMatirx_T = alg.transpose(resourceUserMatirx);
		DoubleMatrix2D tagResourceMatirx_T = alg.transpose(tagResourceMatirx);
		DoubleMatrix2D userTagMatirx_T = alg.transpose(userTagMatirx);		
		while(iterations < maxIterations && residual > maxAllowedResidual){
			//SocialPageRank algorithm
			spr = alg.mult(resourceUserMatirx, //M_PU *
							(alg.mult(userTagMatirx, //M_UA *
									alg.mult(tagResourceMatirx, //M_AP * 
											alg.mult(tagResourceMatirx_T, //M^T_AP *
													alg.mult(userTagMatirx_T, //M^T_UA *
															alg.mult(resourceUserMatirx_T, spr))))))); //M^T_PU * P_i
			
			//Normieren:
			actualSum = alg.norm1(spr);
			residual = 0.0;
			for(int i=0; i < spr.size(); i++){
				value = spr.get(i) / actualSum;
				spr.set(i, (Double.isNaN(value) ? 0.0 : value));
				
				//residual berechnen:
				previousValue = previousSPR.get(i);
				if(!Double.isNaN(value) && !Double.isNaN(previousValue)){
					residual += Math.abs(previousValue - value);
				}
			}
			previousSPR = spr;
			
			iterations++;
			//logger.info("SocialPageRank Iteration " + iterations + " - actual residual: " + residual);
		}
		return spr;
	}
	

	/**
	 * Initializes the matrices that are used to compute the SocialPageRank.
	 * 
	 * @param prefs tell us how to construct the matrices (which data to use)
	 */
	private static void initMatrices(SPRPreference prefs, int indexOfTagOfInterest){
		int maxTagIndex = -1, maxResourceIndex = -1, maxUserIndex = -1;
		ResultSet matrixSize = SelectQueryUtility.executeQuery("SELECT max(t.id), max(r.id), max(u.id) FROM Tag as t, Resource as r, User as u");
		try {
			if(matrixSize.next()){
				//the index of a tag/resource/user within the database will correspond to the index of the tag/resource/user within the matrix...
				maxTagIndex = matrixSize.getInt(1) + 1;
				maxResourceIndex = matrixSize.getInt(2) + 1;
				maxUserIndex = matrixSize.getInt(3) + 1;
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}

		if(indexOfTagOfInterest == NO_TAG_OF_INTEREST){
			resourceUserMatirx = initMatrix(prefs.getResourceUserMatrixQuery(), maxResourceIndex, maxUserIndex);
			tagResourceMatirx = initMatrix(prefs.getTagResourceMatirxQuery(), maxTagIndex, maxResourceIndex);
			userTagMatirx = initMatrix(prefs.getUserTagMatirxQuery(), maxUserIndex, maxTagIndex);
		}else{
			resourceUserMatirx = initMatrix(prefs.getResourceUserMatrixQuery(), maxResourceIndex, maxUserIndex);
			tagResourceMatirx = initTagResourceMatrix(prefs.getTagResourceMatirxQuery(), maxTagIndex, maxResourceIndex, indexOfTagOfInterest);
			userTagMatirx = initUserTagMatrix(prefs.getUserTagMatirxQuery(), maxUserIndex, maxTagIndex, indexOfTagOfInterest);
		}
		
		logger.info("All matrices have been initialized.");
	}
	/**
	 * Creates and inits a matrix with data supplied via executing the query.
	 * @param query the query has to have the form <i>Select rowIndex, columnIndex, value FROM...</i>
	 * @param rows number of rows
	 * @param columns number of columns
	 * @return matrix with data supplied via executing the query
	 */
	public static DoubleMatrix2D initMatrix(String query, int rows, int columns){
		DoubleMatrix2D matrix = new SparseDoubleMatrix2D(rows, columns);
		ResultSet data = SelectQueryUtility.executeQuery(query);
		try {
			while(data.next()){
				matrix.set(data.getInt(1), data.getInt(2), data.getDouble(3));
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return matrix;
	}
	
	/**
	 * Creates and inits a the tagResourceMatrix with data supplied via executing the query while tagOfInterest is emphasized.
	 * @param query the query has to have the form <i>Select rowIndex, columnIndex, value FROM...</i>
	 * @param rows number of rows
	 * @param columns number of columns
	 * @return matrix with data supplied via executing the query
	 */
	public static DoubleMatrix2D initTagResourceMatrix(String query, int rows, int columns, int indexOfTagOfInterest){
		DoubleMatrix2D matrix = new SparseDoubleMatrix2D(rows, columns);
		ResultSet data = SelectQueryUtility.executeQuery(query);
		try {
			while(data.next()){
				matrix.set(data.getInt(1), data.getInt(2), (data.getInt(1) == indexOfTagOfInterest ? (data.getDouble(3) * INFLUENCE_OF_PREFERENCE) : data.getDouble(3)));
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return matrix;
	}
	
	/**
	 * Creates and inits a the userTagMatrix with data supplied via executing the query while tagOfInterest is emphasized.
	 * @param query the query has to have the form <i>Select rowIndex, columnIndex, value FROM...</i>
	 * @param rows number of rows
	 * @param columns number of columns
	 * @return matrix with data supplied via executing the query
	 */
	public static DoubleMatrix2D initUserTagMatrix(String query, int rows, int columns, int indexOfTagOfInterest){
		DoubleMatrix2D matrix = new SparseDoubleMatrix2D(rows, columns);
		ResultSet data = SelectQueryUtility.executeQuery(query);
		try {
			while(data.next()){
				matrix.set(data.getInt(1), data.getInt(2), (data.getInt(2) == indexOfTagOfInterest ? (data.getDouble(3) * INFLUENCE_OF_PREFERENCE) : data.getDouble(3)));
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return matrix;
	}
	
	/**
	 * Just for testing.
	 */
	public static void main(String[] args) {
		List<WeightedResource> ranking = getSocialPageRank(SPRPreference.TRADITIONAL_TAG_ASSIGNMENTS, 100, NO_TAG_OF_INTEREST);
		for (WeightedResource weightedResource : ranking) {
			System.out.println(weightedResource.getWeight() + "  -  " + weightedResource.getIdentifiable().getLabel() + ": " + weightedResource.getIdentifiable().getURI());
		}
	}
	
	/**
	 * The {@link SPRPreference} tell us how to construct the matrices, which build the base in order to compute the SocialPageRank. 
	 */
	public enum SPRPreference {
		/** Only traditional tag assignments, groups are treated as normal resources */
		TRADITIONAL_TAG_ASSIGNMENTS(1, 
				"Select resourceId, userId, count(*) as tags from userTagGroupResource group by resourceId, userId",
				"Select tagId, resourceId, count(*) as users from userTagGroupResource group by resourceId, tagId",
				"Select userId, tagId, count(*) as resources from userTagGroupResource group by userId, tagId")
		
		;
			
			/** the preference strategy indicator */
			private int strategy = -1;
			private String resourceUserMatrixQuery = null;
			private String tagResourceMatirxQuery = null;
			private String userTagMatirxQuery = null;
			private SPRPreference(int strategy, String resourceUserMatrixQuery, String tagResourceMatirxQuery, String userTagMatirxQuery){
				this.strategy = strategy;
				this.resourceUserMatrixQuery = resourceUserMatrixQuery;
				this.tagResourceMatirxQuery = tagResourceMatirxQuery;
				this.userTagMatirxQuery = userTagMatirxQuery;
			}
			/** returns the strategy indicator */
			public int getStrategyIndicator(){
				return this.strategy;
			}
			/** return the query that returns the resourceUserMatrixQuery */
			public String getResourceUserMatrixQuery(){
				return this.resourceUserMatrixQuery;
			}
			
			/** return the query that returns the tagResourceMatirxQuery */
			public String getTagResourceMatirxQuery(){
				return this.tagResourceMatirxQuery;
			}
			
			/** return the query that returns the userTagMatirxQuery */
			public String getUserTagMatirxQuery(){
				return this.userTagMatirxQuery;
			}


	}

}
